package com.study.springsecurity.config;

import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.CharsetUtil;
import com.alibaba.fastjson.JSON;
import com.study.springsecurity.filter.VerificationCodeFilter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;
import org.springframework.stereotype.Component;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;

import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Arrays;

@Slf4j
@Component
public class SecurityConfigurer extends WebSecurityConfigurerAdapter {

    /**
     * 自定义用户认证逻辑
     */
    @Autowired
    private UserDetailsService userDetailsService;

    @Autowired
    private PersistentTokenRepository persistentTokenRepository;

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // 设置没有权限访问跳转自定义页面
        http.exceptionHandling().accessDeniedPage("/unauth.html");

        // 表单登录
        http.formLogin()
                // 登录时传递用户名和密码使用的参数名
                .usernameParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_USERNAME_KEY)
                .passwordParameter(UsernamePasswordAuthenticationFilter.SPRING_SECURITY_FORM_PASSWORD_KEY)

                // 修改默认的登录页为login.html，他会自动去根路径static文件夹下寻找login.html
                .loginPage("/login.html")

                // 设置登录接口地址，这个接口不是真实存在的，还是用的security给我们提供的，之所以要有这个配置，是login.html当中form表单提交的地址我们设置的是这个
                .loginProcessingUrl("/userLogin")

                //defaultSuccessUrl、successForwardUrl、successHandler只能配置一个生效。同理失败也是
                //.defaultSuccessUrl("/index") // 登录成功之后重定向跳转到哪个 url
                //.successForwardUrl("/forward") // 登录成功，转发请求
                .successHandler((request, response, authentication) -> { // 登录成功之后的处理器
                    log.info("登录成功的处理器successHandler，" + JSON.toJSONString(authentication.getPrincipal()));
                    response.sendRedirect(request.getContextPath() + "/index");
                })


                //.failureForwardUrl("/fail") // 登录失败之后重定向跳转到哪个 url
                //.failureUrl() // 登录失败，转发请求
                .failureHandler((request, response, exception) -> { // 登录失败之后的处理器
                    log.error("登录失败处理器failureHandler，原因：" + exception.getMessage());
                    if (exception instanceof BadCredentialsException) {
                        response.setCharacterEncoding(CharsetUtil.UTF_8);
                        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                        ServletOutputStream outputStream = response.getOutputStream();
                        IoUtil.writeUtf8(outputStream, true, "{\"msg\":\"账号密码错误\"}");
                    } else {
                        response.sendRedirect(request.getContextPath() + "/login.html");
                    }
                })

                .permitAll(); // permitAll中文意思是许可所有的：所有的都遵循上面的配置的意思


        http.authorizeRequests()
                // 对于登录接口允许匿名访问
                .antMatchers("/user/login").anonymous()

                // 该路由不需要身份认证
                .antMatchers("/getVerifyCode", "/userLogin", "/login.html", "/h2", "/h2/**").permitAll()

                // 需要用户带有管理员权限
                .antMatchers("/findAll").hasRole("管理员")
                .antMatchers("/find").hasRole("管理员")

                // 需要用户具备这个接口的权限
                .antMatchers("/find").hasAuthority("menu:user")

                // 任何请求都需要认证
                .anyRequest().authenticated();

        // 退出,这里的/logout的请求是和前端的接口约定，是security给我们提供的,退出成功后跳转登录页/login.html
        http.logout().logoutUrl("/logout")
                //.logoutSuccessHandler() // 配置登出处理器
                //.defaultLogoutSuccessHandlerFor() // 根据登出的URL匹配对应的handler处理登出逻辑
                .logoutSuccessUrl("/login.html").permitAll();


        http.rememberMe() // 设置记住我
                .tokenRepository(persistentTokenRepository) // 配置token存储库
                // 设置有效时长180秒，默认 2 周时间。
                .tokenValiditySeconds(180)
                .userDetailsService(userDetailsService);

        http.sessionManagement() // 回话管理器
                .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) // 指定session创建策略

                .maximumSessions(1) // 同一个用户的最大会话数（同一个账号允许同时登录多个客户端的数量）
                .maxSessionsPreventsLogin(false) // 达到回话限制时的处理，true：当前会话无法登录成功  false：之前的会话失效
                .expiredSessionStrategy(event -> {
                    log.error("该账号已在别处登录");
                    HttpServletRequest request = event.getRequest();
                    HttpServletResponse response = event.getResponse();
                    response.setCharacterEncoding(CharsetUtil.UTF_8);
                    response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    ServletOutputStream outputStream = response.getOutputStream();
                    IoUtil.writeUtf8(outputStream, true, "{\"msg\":\"该账号已在别处登录\"}");
                })
                .sessionRegistry(sessionRegistry); // 指定session注册器

        // 关闭 csrf
        http.csrf().disable();

        // https://blog.csdn.net/fd2025/article/details/124491570
        http.headers().frameOptions().sameOrigin(); // 页面只能被本站页面嵌入到iframe或者frame中

        http.cors().configurationSource(corsConfigurationSource());

        // 在用户密码认证Filter之前新增验证码检查的Filter
        http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    CorsConfigurationSource corsConfigurationSource() {
        CorsConfiguration corsConfiguration = new CorsConfiguration();
        corsConfiguration.setAllowedHeaders(Arrays.asList("*"));
        corsConfiguration.setAllowedMethods(Arrays.asList("*"));
        corsConfiguration.setAllowedOrigins(Arrays.asList("*"));
        corsConfiguration.setMaxAge(3600L);
        UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
        source.registerCorsConfiguration("/**", corsConfiguration);
        return source;
    }

    /**
     * 身份认证接口
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 告诉SpringSecurity 我们要使用自己定义的userDetailsService来通过username来获取主体，
        // 并且使用了BCryptPasswordEncoder加密进行密码比较
        auth.userDetailsService(userDetailsService)
                .passwordEncoder(new BCryptPasswordEncoder());
    }
}
